/* --------------------------------------------------------
 * Copyright (c) aeky , Inc.  All rights reserved.
 * --------------------------------------------------------
 */
package com.aeky.server.support;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.UndeclaredThrowableException;
import java.util.Map;

import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.web.context.ServletConfigAware;
import org.springframework.web.context.ServletContextAware;

import com.google.gwt.user.client.rpc.RemoteService;
import com.google.gwt.user.client.rpc.SerializationException;
import com.google.gwt.user.server.rpc.RPC;
import com.google.gwt.user.server.rpc.RPCRequest;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;

/**
 * RPC Service Exporter from Gilead project.
 * 
 */
@SuppressWarnings("serial")
public class GWTRPCServiceExporter extends RemoteServiceServlet implements RPCServiceExporter, ServletContextAware,
        ServletConfigAware, BeanNameAware {

    /** The Constant LOG. */
    private static final Logger LOG = LoggerFactory.getLogger(GWTRPCServiceExporter.class);

    /** Disable RPC response compression. Value is 0. */
    public static final int COMPRESSION_DISABLED = 0;

    /** Leave default RPC response compression behavior. Value is 1. */

    public static final int COMPRESSION_AUTO = 1;

    /** Force compression of all RPC responses. Value is 2. */
    public static final int COMPRESSION_ENABLED = 2;

    /** The service interfaces. */
    private Class<?>[] serviceInterfaces;

    /** The service. */
    private Object service = this;

    /** The servlet context. */
    private ServletContext servletContext;

    /** The compress response. */
    private int compressResponse = COMPRESSION_AUTO;

    /** The disable response caching. */
    private boolean disableResponseCaching = false;

    /** The throw undeclared exception to servlet container. */
    private boolean throwUndeclaredExceptionToServletContainer = false;

    /** The bean name. */
    private String beanName = "GWTRPCServiceExporter";

    /**
     * {@inheritDoc}
     * 
     * @see com.google.gwt.user.server.rpc.RemoteServiceServlet#doUnexpectedFailure
     *      (java.lang.Throwable)
     */
    @Override
    protected void doUnexpectedFailure(Throwable e) {
        super.doUnexpectedFailure(e);
        if (throwUndeclaredExceptionToServletContainer) {
            throw new RuntimeException(e);
        }
    }

    /*
     * Concurrent put/get invocations are reasonably safe in this use case on an
     * ImmutableCopyMap.
     */
    /** The method cache. */
    private Map<Method, Method> methodCache = new ImmutableCopyMap<Method, Method>();

    /**
     * Disables HTTP response caching by modifying response headers for
     * browsers. Can be overridden by extending classes to change behaviour.
     * 
     * @param request http request
     * @param response http response
     * 
     * @throws UnsupportedEncodingException the unsupported encoding exception
     */
    private void preprocessHTTP(HttpServletRequest request, HttpServletResponse response)
            throws UnsupportedEncodingException {
        if (disableResponseCaching) {
            ServletUtils.disableResponseCaching(response);
        }
    }

    /**
     * Implementation of {@link ServletContextAware}, is invoked by the Spring
     * application context.
     * 
     * @param servletContext servlet context
     */
    public void setServletContext(ServletContext servletContext) {
        this.servletContext = servletContext;
    }

    /**
     * Returns the servlet context.
     * 
     * @return {@link ServletContext}
     */
    @Override
    public ServletContext getServletContext() {
        return servletContext;
    }

    /**
     * {@inheritDoc}
     * 
     * @see com.google.gwt.user.server.rpc.RemoteServiceServlet#onAfterResponseSerialized
     *      (java.lang.String)
     */
    @Override
    protected void onAfterResponseSerialized(String serializedResponse) {
        if (LOG.isTraceEnabled()) {
            LOG.trace("Serialised RPC response: [" + serializedResponse + "]");
        }
    }

    /*
     * 
     * @seecom.google.gwt.user.server.rpc.RemoteServiceServlet#
     * onBeforeRequestDeserialized(java.lang.String)
     */
    @Override
    protected void onBeforeRequestDeserialized(String serializedRequest) {
        if (LOG.isTraceEnabled()) {
            LOG.trace("Serialised RPC request: [" + serializedRequest + "]");
        }
    }

    /**
     * Handles method invocation on a service and is invoked by
     * {@link #processCall(String)}.
     * 
     * @param service Service to invoke method on
     * @param targetMethod Method to invoke.
     * @param targetParameters Parameters to pass to method. Can be null for no
     *            arguments.
     * @param rpcRequest RPCRequest instance for this request
     * 
     * @return Return RPC encoded result.
     * 
     * @throws Exception exception
     */
    protected String invokeMethodOnService(Object service, Method targetMethod, Object[] targetParameters,
            RPCRequest rpcRequest) throws Exception {
        Object result = targetMethod.invoke(service, targetParameters);
        String encodedResult = RPC.encodeResponseForSuccess(rpcRequest.getMethod(), result,
                rpcRequest.getSerializationPolicy());
        return encodedResult;
    }

    /**
     * Handles exceptions thrown by the target service, which are wrapped in
     * {@link InvocationTargetException}s due to invocation by reflection. This
     * method is invoked by {@link #processCall(String)}. This implementation
     * encodes exceptions as RPC errors and returns them. For details on
     * arguments please consult
     * {@link #invokeMethodOnService(Object, Method, Object[], RPCRequest)}.
     * 
     * @param e Exception thrown
     * @param service service
     * @param targetMethod target method
     * @param rpcRequest RPC request
     * 
     * @return RPC payload
     * 
     * @throws SerializationException SerializationException
     */
    protected String handleInvocationTargetException(Exception e, Object service, Method targetMethod,
            RPCRequest rpcRequest) throws SerializationException {

        Throwable cause = e.getCause();
        if (cause == null) {
            cause = e;
        }
        if (cause instanceof RuntimeException && null != cause.getCause()) {
            cause = cause.getCause();
        }
        String failurePayload = RPC.encodeResponseForFailure(rpcRequest.getMethod(), cause,
                rpcRequest.getSerializationPolicy());

        return failurePayload;
    }

    /**
     * Handles {@link UndeclaredThrowableException}s which are thrown by the
     * target service. This method is invoked by {@link #processCall(String)}.
     * This implementation re-casts 'e'. For details on arguments please consult
     * {@link #invokeMethodOnService(Object, Method, Object[], RPCRequest)}.
     * 
     * @param e Exception thrown
     * @param service service
     * @param targetMethod targe method
     * @param rpcRequest RPC request
     * 
     * @return RPC payload
     * 
     * @throws BfpException exception converted
     */
    protected String handleUndeclaredThrowableException(Exception e, Object service, Method targetMethod,
            RPCRequest rpcRequest) throws Exception {
        throw new Exception("serverException");
    }

    /**
     * Returns method to invoke on service. This implementation calls
     * 
     * @param decodedMethod Method as determined by RPC
     * 
     * @return Method to invoke.
     * 
     * @throws NoSuchMethodException NoSuchMethodException
     */
    protected Method getMethodToInvoke(Method decodedMethod) throws NoSuchMethodException {
        Method method = methodCache.get(decodedMethod);
        if (method != null) {
            return method;
        }
        method = ReflectionUtils.getRPCMethod(service, serviceInterfaces, decodedMethod);
        return methodCache.put(decodedMethod, method);
    }

    /**
     * Overridden from {@link RemoteServiceServlet} and invoked by the servlet
     * code.
     * 
     * @param payload payload
     * 
     * @return String return value
     * 
     * @throws SerializationException SerializationException
     */
    @Override
    public String processCall(String payload) throws SerializationException {
        RPCRequest rpcRequest = RPC.decodeRequest(payload, null, this);
        Method targetMethod;
        try {
            targetMethod = getMethodToInvoke(rpcRequest.getMethod());
            Object[] targetParameters = rpcRequest.getParameters();
            try {
                return invokeMethodOnService(service, targetMethod, targetParameters, rpcRequest);
            } catch (Exception e) {
                return handleInvocationTargetException(e, service, targetMethod, rpcRequest);
            }
        } catch (Exception ex) {
            return handleServiceException(ex, rpcRequest);
        }
    }

    /**
     * Handle service exception.
     * 
     * @param e the e
     * @param rpcRequest the rpc request
     * 
     * @return the string
     * 
     * @throws SerializationException the serialization exception
     */
    protected String handleServiceException(Exception e, RPCRequest rpcRequest) throws SerializationException {
        LOG.error("Handle service exception:", e);
        return RPC.encodeResponseForFailure(null, e, rpcRequest.getSerializationPolicy());
    }

    /**
     * Set the wrapped service bean. RPC requests are decoded and the
     * corresponding method of the service object is invoked.
     * 
     * @param service Service to which the decoded requests are forwarded
     */
    public void setService(Object service) {
        this.service = service;
    }

    /**
     * Implementation of inherited interface.
     * 
     * @param request request
     * @param response response
     * 
     * @throws ServletException exception
     * @throws IOException IO Exception
     */
    public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException,
            IOException {
        try {
            preprocessHTTP(request, response);
            ServletUtils.setRequest(request);
            ServletUtils.setResponse(response);
            doPost(request, response);
        } finally {
            ServletUtils.setRequest(null);
            ServletUtils.setResponse(null);
        }
    }

    /**
     * Specifies the interfaces which must be implemented by the service bean.
     * If not specified then any interface extending {@link RemoteService} which
     * is implemented by the service bean is assumed. Implementation note: as
     * methods are only lazily bound to the service implementation you may get
     * away with mismatches between the specified interfaces and the actual
     * implementation as long as no method is invoked which has a
     * different/missing signature in the interface and the service
     * implementation.
     * 
     * @param serviceInterfaces service interface
     */
    public void setServiceInterfaces(Class<RemoteService>[] serviceInterfaces) {
        this.serviceInterfaces = serviceInterfaces;
    }

    /**
     * Should be invoked after all properties have been set. Normally invoked by
     * the Spring application context setup.
     * 
     * @throws Exception exception
     * 
     * @see InitializingBean#afterPropertiesSet()
     */
    public void afterPropertiesSet() throws Exception {
        if (service == null) {
            throw new Exception("You must specify a service object.");
        }
        if (serviceInterfaces == null) {
            LOG.debug("Discovering service interfaces");
            serviceInterfaces = ReflectionUtils.getExposedInterfaces(service.getClass());
            if (serviceInterfaces.length == 0) {
                LOG.warn("The specified service does neither implement RemoteService "
                        + "nor were any service interfaces specified. RPC access to *all* object methods is allowed.");
            }
        }
    }

    /**
     * Return target service. Each {@link GWTRPCServiceExporter} has a single
     * target service which it redirects RPC to.
     * 
     * @return Object
     */
    public Object getService() {
        return service;
    }

    /**
     * {@inheritDoc}
     * 
     * @see com.google.gwt.user.server.rpc.RemoteServiceServlet#shouldCompressResponse
     *      (javax.servlet.http.HttpServletRequest,
     *      javax.servlet.http.HttpServletResponse, java.lang.String)
     */
    @Override
    protected boolean shouldCompressResponse(HttpServletRequest request, HttpServletResponse response,
            String responsePayload) {
        switch (compressResponse) {
        case COMPRESSION_DISABLED:
            return false;
        case COMPRESSION_ENABLED:
            return true;
        default:
            break;
        }
        return super.shouldCompressResponse(request, response, responsePayload);
    }

    /**
     * Enables or disables compression of RPC output. Defaults to
     * {@link #COMPRESSION_AUTO}. Allowed values are
     * {@link #COMPRESSION_ENABLED}, {@link #COMPRESSION_DISABLED} and
     * {@link #COMPRESSION_AUTO}.
     * 
     * @param compressResponse compress response code
     */
    protected void setCompressResponse(int compressResponse) {
        if (compressResponse != COMPRESSION_ENABLED && compressResponse != COMPRESSION_DISABLED
                && compressResponse != COMPRESSION_AUTO) {
            throw new IllegalArgumentException("Invalid compressResponse argumnet " + compressResponse);
        }
        this.compressResponse = compressResponse;
    }

    /**
     * Can be used to set HTTP response headers that explicitly disable caching
     * on the browser side. Note that due to the additional headers the response
     * size increases.
     * 
     * @param disableResponseCaching disableResponseCaching
     */
    public void setResponseCachingDisabled(boolean disableResponseCaching) {
        this.disableResponseCaching = disableResponseCaching;
    }

    /**
     * When enabled will throw exceptions which originate from the service and
     * have not been declared in the RPC interface back to the servlet
     * container.
     * 
     * @param throwUndeclaredExceptionToServletContainer Defaults to
     *            <code>false</code>
     */
    public void setThrowUndeclaredExceptionToServletContainer(boolean throwUndeclaredExceptionToServletContainer) {
        this.throwUndeclaredExceptionToServletContainer = throwUndeclaredExceptionToServletContainer;
    }

    /**
     * Setter for servlet configuration.
     * 
     * @param servletConfig servletConfig
     */
    public void setServletConfig(ServletConfig servletConfig) {
        try {
            init(servletConfig);
        } catch (ServletException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * set bean name.
     * 
     * @param beanName bean name
     */
    public void setBeanName(String beanName) {
        this.beanName = beanName;
    }

    /**
     * {@inheritDoc}
     * 
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        return beanName;
    }

}
